/**
 * Capable of writing out an XML report, by delegating the call to
 * generateReport() to an extended TestNG reporter.
 */
package com.googlecode.lightest.core

import groovy.xml.StreamingMarkupBuilder

import org.testng.ISuite
import org.testng.ITestResult
import org.testng.xml.XmlSuite
import org.testng.reporters.FailedReporter
import org.testng.reporters.XMLUtils

class LightestReporter extends LightestTestListener
    implements ILightestReporter
{
    def outputDir
    def refreshReport
    def generateHTML
    def xmlReporter
    def failedReporter
    def transformer

    LightestReporter() {
        refreshReport = false
        generateHTML = true
        xmlReporter = new XMLReporter2()
        failedReporter = new FailedReporter()
        transformer = new LightestTransformer()
    }

    @Override
    void onTestStart(ITestResult result) {
        super.onTestStart(result)

        if (refreshReport) {
            generateReport(outputDir)
        }
    }

    /**
     * Sets the output directory to be used for report generation when none
     * is explicitly specified.
     */
    void setOutputDir(String outputDir) {
        this.outputDir = outputDir
    }

    /**
     * Sets whether to refresh the report before each test is run. If set to
     * true, the report will be created in the directory that getOutputDir()
     * returns.
     *
     * @param refreshReport
     */
    void setRefreshReport(boolean refreshReport) {
        this.refreshReport = refreshReport
    }
    
    /**
     * Sets whether to generate the HTML report along with the standard TestNG
     * XML report.
     * 
     * @param generateHTML
     */
    void setGenerateHTML(boolean generateHTML) {
        this.generateHTML = generateHTML
    }

    String getOutputDir() {
        return outputDir
    }

    /**
     * A wrapper for the parameterized generateReport() method.
     */
    void generateReport(String outputDir) {
        assert outputDir != null

        List<ITestContext> contexts = getTestContexts()
        List<ISuite> suites = contexts.collect { it.getSuite() }
        List<XmlSuite> xmlSuites = suites.collect { it.getXmlSuite() }

        generateReport(xmlSuites, suites, outputDir)
    }

    /**
     * Generates a report in the specified output directory. This will include
     * the standard testng-results.xml, testng-failed.xml, as well as a framed
     * HTML report. Any existing results in the output directory will be
     * deleted.
     *
     * @param xmlSuites
     * @param suites
     * @param outputDirectory
     */
    void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites,
        String outputDirectory)
    {
        def outputDir = new File(outputDirectory)

        if (outputDir.exists()) {
            outputDir.eachFile {
                it.delete()
            }
        }
        
        xmlReporter.setEnvironmentMap(getEnvironmentMap())
        xmlReporter.setTaskResultMap(getTaskResultMap())
        xmlReporter.generateReport(xmlSuites, suites, outputDirectory)
        
        failedReporter.generateReport(xmlSuites, suites, outputDirectory)

        if (generateHTML) {
            def resultsFile = new File(outputDir, 'testng-results.xml')
            createHtmlReport(outputDir, resultsFile)
        }
    }

    /**
     * Transforms the testng-results.xml file to a user-friendly HTML report.
     * This implementation comes from the testng-xslt maven plugin mojo. Then
     * fills in the details for each test run by building a detailed
     * task-oriented report, linked to from the former.
     *
     * @param outputDir     the output directory specified when running the
     *                      TestNG tests
     * @param tetsNGResult  the testng-results.xml file to transform
     *
     * @see <a href="http://code.google.com/p/testng-xslt/">TestNG XSLT</a>
     */
    protected void createHtmlReport(File outputDir, File testNGResult) {
        copyResources(outputDir)
        createHtmlReportBase(outputDir, testNGResult)
        createHtmlReportDetails(outputDir, testNGResult)
    }
    
    protected void copyResources(File outputDir) {
        def resources = [
            'jquery-1.2.6.min.js',
            'jquery.hoverIntent.js',
            'lightest-report.css',
            'lightest-base.js',
            'lightest-details.js'
        ]

        for (resource in resources) {
            def stream = this.class.getResourceAsStream("/${resource}")
            def file = new File(outputDir, resource)
            file.text = stream.text
        }
    }

    protected void createHtmlReportBase(File outputDir, File testNGResult) {
        def is = new FileInputStream(testNGResult)
        def os = new FileOutputStream(new File(outputDir, 'index.html'))
        
        transformer.setParameter('testNgXslt.outputDir', outputDir.getPath())
        transformer.setParameter('testNgXslt.reportTitle', 'Lightest Results')
        transformer.transform(is, os)
    }
    
    /**
     * Creates a task report file for each test method invocation represented
     * in the TestNG results XML.
     * 
     * @param outputDir     the directory in which to create the files
     * @param testNGResult  the testng-results.xml file on which to base report
     * 					    details generation
     */
    protected void createHtmlReportDetails(File outputDir, File testNGResult) {
        def root = new XmlParser().parse(testNGResult)
        def envMap = [:]
        def fileNameMap = [:]
        def statusMap = [ PASS: 'passed', FAIL: 'failed', SKIP: 'skiped' ]
        // "skipped" is misspelled in the TestNG XSLT file!
        
        for (envNode in root.envs[0].env) {
            envMap[envNode.'@id'] = envNode
        }
        
        for (suiteNode in root.suite) {
            def statusCounts = [ PASS: 0, FAIL: 0, SKIP: 0 ]
        
            for (classNode in suiteNode.test.'class') {
                def className = classNode.'@name'
                def env = envMap[classNode.'@env']
                def classCount = 0
                
                for (methodNode in classNode.'test-method') {
                    def status = methodNode.'@status'
                    
                    ++statusCounts[status]
                    ++classCount
                    
                    if (methodNode?.'@is-config' == 'true') {
                        continue
                    }
                
                    def builder = new StreamingMarkupBuilder()
                    def markup = {
                        html {
                            head {
                                link (href: 'style.css', rel: 'stylesheet')
                                link (href: 'lightest-report.css', rel: 'stylesheet')
                                script (type: 'text/javascript', src: 'jquery-1.2.6.min.js') {}
                                script (type: 'text/javascript', src: 'jquery.hoverIntent.js') {}
                                script (type: 'text/javascript', src: 'lightest-details.js') {}
                            }
                            body {
                                out << getEnvMarkup(env)
                                
                                for (node in methodNode.'task-result') {
                                    table (class: 'task-container') {
                                        tr {
                                            td (class: 'task-main') {
                                                out << getTaskMarkup(node)
                                            }
                                            td (class: 'task-spacer')
                                        }
                                    }
                                }
                            }
                        }
                    }
                    
                    def prefix = "${className}_${methodNode.'@name'}"
                    def byStatus = "byStatus_${statusMap[status]}_${status}${statusCounts[status]}_details"
                    def byClass = "byClass_${status}${classCount}_details"
                    def path = "${prefix}_${classCount}.html"
                    def detailsFile = new File(outputDir, path)
                    
                    fileNameMap["${prefix}_${byStatus}"] = path
                    fileNameMap["${prefix}_${byClass}"] = path
                    
                    detailsFile.text = builder.bind(markup)
                }
            }
        }
        
        // TODO - we don't escape anything in the JSON here ... do we need to?
        def mappings = fileNameMap.collect { k, v -> "'${k}':'${v}'" }
        def fileNameFile = new File(outputDir, 'lightest-filenames.js')
        
        fileNameFile.text = "var fileNameMap = {${mappings.join(',')}};"
    }
    
    protected getEnvMarkup(node) {
        assert node.name() == 'env'
        
        def envMarkup = {
            div (class: 'env-settings') {
                table {
                    tr {
                        td (class: 'env-title', "${node.'@id'}:")
                        td (class: 'env-table') {
                            table {
                                tr {
                                    th ('Setting')
                                    th ('Value')
                                }
                                for (setting in node.children()) {
                                    tr {
                                        td (setting.name())
                                        td (setting.value())
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        
        return envMarkup
    }
    
    /**
     * Recursively builds a Closure representing markup for a task results, and
     * returns the result.
     * 
     * @param node  a task result node from the testng-results.xml
     */
    protected getTaskMarkup(node) {
        assert node.name() == 'task-result'
        
        def taskMarkup = {
            div (class: 'task') {
                table (class: 'task-result') {
                    tr (class: 'task-header') {
                        th (class: 'task-name', node.'@name')
                        th (class: 'task-params', node.'@params')
                        th (class: 'task-duration', formatDuration(node.'@duration-ms'))
                        th (class: 'task-emoticon', getEmoticon(node.'@status'))
                        th (class: 'task-display-level')
                    }
                    tr (class: 'task-info') {
                        td ('Description')
                        td (colspan: '4', node.'@description')
                    }
                    tr (class: 'task-info', style: 'display: none') {
                        td ('Status')
                        td (class: 'task-status', colspan: '4', node.'@status')
                    }
                    tr (class: 'task-info') {
                        td ('Message')
                        td (colspan: '4', node.'@message')
                    }
                    tr (class: 'task-info') {
                        td ('Response Data')
                        td (colspan: '4', node.'@response-data')
                    }
                    if (node.'@links') {
                        tr (class: 'task-info') {
                            td ('Links')
                            td (colspan: '4') {
                                ul (class: 'task-links-list') {
                                    for (link in node.'@links'.split(/,/)) {
                                        li {
                                            a (href: link, link)
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                div (class: 'nested-results') {
                    for (nested in node.'nested-results'.'task-result') {
                        out << getTaskMarkup(nested)
                    }
                }
            }
        }
        
        return taskMarkup
    }

    /**
     * Returns a duration formatted for a report, the same way TestNG XSLT
     * would.
     * 
     * @param durationMs  a length of time, in milliseconds
     */
    protected String formatDuration(durationMs) {
        if (durationMs instanceof String) {
            durationMs = Integer.parseInt(durationMs)
        }
        
        def remainingMs = durationMs
        def durationUnits = []
        
        if (remainingMs > 86400000) {
            durationUnits << "${remainingMs % 86400000}d"
            remainingMs %= 86400000
        }
        if (remainingMs > 3600000) {
            durationUnits << "${remainingMs % 3600000}h"
            remainingMs %= 3600000
        }
        if (remainingMs > 60000) {
            durationUnits << "${remainingMs % 60000}m"
            remainingMs %= 60000
        }
        if (remainingMs > 1000) {
            durationUnits << "${remainingMs % 1000}s"
            remainingMs %= 1000
        }
        if (durationMs < 1000) {
            durationUnits << "${remainingMs}ms"
        }
        
        return durationUnits.join(' ')
    }
    
    protected String getEmoticon(status) {
        if (status instanceof String) {
            status = Integer.parseInt(status)
        }
        
        switch (status) {
            case ITaskResult.STATUS_OK:      return ':)'
            case ITaskResult.STATUS_FLAGGED: return ':?'
            case ITaskResult.STATUS_FAILED:  return ':('
            case ITaskResult.STATUS_DOOMED:  return ':O'
        }
        
        return ""
    }
}
